//
// Created by yanhai on 2020/2/18.
//

/**
 * explicit
 *  1.C++中的explicit关键字用于修饰有一个或多个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的,
 *  2.类构造函数默认情况下即声明为implicit(隐式).
 *  那么显示声明的构造函数和隐式声明的有什么区别呢? 我们来看下面的例子:
 */

#include <iostream>
#include <cstring>
#include <memory>

class CxString  // 没有使用explicit关键字的类声明, 即默认为隐式声明
{
public:
    CxString(int size)
    {
        _size = size;                // string的预设大小
        _pstr = new char[_size + 1];    // 分配string的内存
        memset(_pstr, 0, _size + 1);
    }

    CxString(const char *p)
    {
        _size = strlen(p);
        _pstr = new char[_size + 1];    // 分配string的内存
        strcpy(_pstr, p);            // 复制字符串
    }

    ~CxString()
    {
        delete[] _pstr;
    }

private:
    char *_pstr;
    int _size;
};

// 下面是调用:
void test1()
{
    CxString string1(24);     // 这样是OK的, 为CxString预分配24字节的大小的内存
    CxString string2 = 10;    // 这样是OK的, 为CxString预分配10字节的大小的内存
//    CxString string3;         // 这样是不行的, 因为没有默认构造函数, 错误为: “CxString”: 没有合适的默认构造函数可用
    CxString string4("aaaa"); // 这样是OK的
    CxString string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
    CxString string6 = 'c';   // 这样也是OK的, 其实调用的是CxString(int size), 且size等于'c'的ascii码
//    string1 = 2;              // 这样也是OK的, 为CxString预分配2字节的大小的内存
//    string2 = 3;              // 这样也是OK的, 为CxString预分配3字节的大小的内存
//    string4 = string1;        // 这样也是OK的, 至少编译是没问题的, 但是如果析构函数里用free释放_pstr内存指针的时候可能会报错, 完整的代码必须重载运算符"=", 并在其中处理内存释放
}

/**
 * 上面的代码中, "CxString string2 = 10;" 这句为什么是可以的呢?
 *   在C++中, 如果的构造函数只有一个参数时, 那么在编译的时候就会有一个缺省的转换操作:
 *   将该构造函数对应数据类型的数据转换为该类对象.
 *   也就是说 "CxString string2 = 10;" 这段代码, 编译器自动将整型转换为CxString类对象,
 *   实际上等同于下面的操作:
 *
 *      CxString string2(10);
 *                或
 *      CxString temp(10);
 *      CxString string2 = temp;
 * 但是, 上面的代码中的_size代表的是字符串内存分配的大小,
 * 那么调用的第二句 "CxString string2 = 10;" 和第六句 "CxString string6 = 'c';" 就显得不伦不类,
 * 而且容易让人疑惑. 有什么办法阻止这种用法呢? 答案就是使用explicit关键字. 我们把上面的代码修改一下, 如下:
 */
class CxString2  // 没有使用explicit关键字的类声明, 即默认为隐式声明
{
public:
    explicit CxString2(int size)
    {
        _size = size;                // string的预设大小
        _pstr = new char[_size + 1];    // 分配string的内存
        memset(_pstr, 0, _size + 1);
    }

    explicit CxString2(const char *p)
    {
        _size = strlen(p);
        _pstr = new char[_size + 1];    // 分配string的内存
        strcpy(_pstr, p);            // 复制字符串
    }

    ~CxString2()
    {
        delete[] _pstr;
    }

private:
    char *_pstr;
    int _size;
};

// 下面是调用:
void test2()
{
    CxString2 string1(24);     // 这样是OK的
//    CxString2 string2 = 10;    // 这样是不行的, 因为explicit关键字取消了隐式转换
//    CxString2 string3;         // 这样是不行的, 因为没有默认构造函数
    CxString2 string4("aaaa"); // 这样是OK的
//    CxString2 string5 = "bbb"; // 这样也是OK的, 调用的是CxString(const char *p)
//    CxString2 string6 = 'c';   // 这样是不行的, 其实调用的是CxString(int size), 且size等于'c'的ascii码, 但explicit关键字取消了隐式转换
//    string1 = 2;              // 这样也是不行的, 因为取消了隐式转换
//    string2 = 3;              // 这样也是不行的, 因为取消了隐式转换
//    string3 = string1;        // 这样也是不行的, 因为取消了隐式转换, 除非类实现操作符"="的重载
}

/**
 * 对于有多个参数的构造函数，explicit的影响也关系重大，
 * 使用explicit修饰，可以令“多数值自动类型转换”不再起作用，即使初始化以=语法进行。
 * 同样的，explicit 构造函数如果接受的是个初始列，会失去“初始列带0个、1个或多个初值”的隐式转换能力
 */
class CxString3 {
public:
    explicit CxString3(int size)
    {
        _size = size;                // string的预设大小
        _pstr = new char[_size + 1];    // 分配string的内存
        memset(_pstr, 0, _size + 1);
    }

    explicit CxString3(const char *p)
    {
        _size = strlen(p);
        _pstr = new char[_size + 1];    // 分配string的内存
        strcpy(_pstr, p);            // 复制字符串
    }

    /**
     * 加上 explicit 关键字，下面方式调用将会编译错误
     *      CxString3 string6 = {10, 20, 30};
     * @param a
     * @param b
     * @param c
     */
    explicit CxString3(int a, int b, int c)
    {
        _size = a;
        _pstr = nullptr;
    }

    ~CxString3()
    {
        delete[] _pstr;
    }

private:
    char *_pstr;
    int _size;
};

void test3()
{
    CxString3 string1(24);     // 这样是OK的
//    CxString3 string2 = 10;    // 这样是不行的, 因为explicit关键字取消了隐式转换
//    CxString3 string3;         // 这样是不行的, 因为没有默认构造函数
    CxString3 string4("aaaa"); // 这样是OK的
    CxString3 string5{10, 20, 30};      // 调用  CxString3(int a, int b, int c)
//    CxString3 string6 = {10, 20, 30};   //  编译错误
}

/**
 * explicit关键字的作用就是防止类构造函数的隐式自动转换.
 */
int main()
{
    test1();
    test2();
    test3();
    return 0;
}

/**
 * 总结
 *  - explicit关键字用于修饰有一个或多个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的,
 *  - google的c++规范中提到explicit的优点是可以避免不合时宜的类型变换，缺点无。
 *    所以google约定所有单参数的构造函数都必须是显示的，只有极少数情况下拷贝构造函数可以不声明称explicit。
 *    例如作为其他类的透明包装器的类。
 *  - effective c++中说：被声明为explicit的构造函数通常比其non-explicit兄弟更受欢迎。
 *    因为它们禁止编译器执行非预期（往往也不被期望）的类型转换。除非我有一个好理由允许构造函数被用于隐式类型转换，
 *    否则我会把它声明为explicit，鼓励大家遵循相同的政策。
 *
 * 参考 https://www.cnblogs.com/rednodel/p/9299251.html
 */
